﻿namespace MyCompany.Models {
	using System;
	using System.Diagnostics;
	using System.IO;
	using System.Net;
	using System.Windows.Threading;

	using MyCompany.Core;
	using MyCompany.Interfaces;

	using SolidMvvm;

	using SolidMvvmInterfaces;

	/// <summary>
	///     A simple http server that calls back to the owner when a request comes in.
	///     It handles browser requests for favicon.ico automatically.
	/// </summary>
	public class LocalHostHttpServer : ModelBase, ILocalHostHttpServer {
		HttpListener httpListener;

		/// <summary>Constructor</summary>
		/// <param name="favIconPathRelativeToBinFolder">
		///     Usually copied to the Bin folder
		///     of the application project. "./favicon.ico" for example.
		/// </param>
		/// <param name="port">The HTTP listener's port such as 3000.</param>
		/// <param name="requestDelegate">Callback when a request is received.</param>
		/// <param name="messageReporter">
		///     Any message reporter. It will only be invoked on the owning thread,
		///     so assuming that it's the UI thread, it's safe to put the message in the GUI.
		/// </param>
		/// <param name="localIPAddressWithoutHttpPrefix">For example, "10.0.1.147".</param>
		public LocalHostHttpServer(string favIconPathRelativeToBinFolder, string localIPAddressWithoutHttpPrefix, int port, RequestDelegate requestDelegate, IMessageReporter messageReporter) {
			try {
				LocalIPAddressWithoutHttpPrefix = localIPAddressWithoutHttpPrefix;
				Port = port;
				FavIconPathRelativeToBinFolder = favIconPathRelativeToBinFolder;
				Request = requestDelegate;
				MessageReporter = messageReporter;

				if (!HttpListener.IsSupported)
					throw new GeneralException("Windows XP SP2 or Server 2003 (or later) is required to use an http listener.", null);

				httpListener = new HttpListener();
				httpListener.Prefixes.Add(Utilities.GetFullHttpAddressWithPort(LocalIPAddressWithoutHttpPrefix, Port));
				//httpListener.Prefixes.Add("http://10.0.1.147:3000/");
				httpListener.Start();

				// Listen for a single request on another thread. Once we receive one, we call BeginGetContext again.
				httpListener.BeginGetContext(HandleRequestOnWorkerThread, httpListener);

				MessageReporter.ReportMessage("HTTP server listening at " + Utilities.GetFullHttpAddressWithPort(LocalIPAddressWithoutHttpPrefix, Port) + ".", null, MessageType.Console);
			}
			catch (Exception exception) {
				Cleaner.CleanConstructor(this, exception);
			}
		}

		public override void Dispose() {
			GC.SuppressFinalize(this);

			var disposeErrors = String.Empty;

			if (httpListener != null)
				try {
					// NOTE: If you call Start() on a listener and there is an address conflict,
					// for some reason, the listener now reports to be disposed. We avoid
					// further accessing the disposed object by checking IsListening.
					if (httpListener.IsListening) {
						httpListener.Stop();
						if (httpListener.Prefixes.Contains(Utilities.GetFullHttpAddressWithPort(LocalIPAddressWithoutHttpPrefix, Port)))
							httpListener.Prefixes.Remove(Utilities.GetFullHttpAddressWithPort(LocalIPAddressWithoutHttpPrefix, Port));
					}
				}
				catch (Exception exception) {
					disposeErrors += exception.Message + "\n";
				}
			httpListener = null;

			Request = null;
			MessageReporter = null;
			FavIconPathRelativeToBinFolder = null;
			Port = -1;
			LocalIPAddressWithoutHttpPrefix = null;

			try {
				base.Dispose();
			}
			catch (Exception exception) {
				disposeErrors += exception.Message + "\n";
			}

			if (disposeErrors != String.Empty)
				throw new CleanupException(disposeErrors.Trim(), null);
		}

		public string LocalIPAddressWithoutHttpPrefix { get; private set; }

		public int Port { get; private set; }

		//int port;
		//public int Port
		//{
		//    get { return port; }
		//    set
		//    {
		//        if (port == value)
		//            return;

		//        port = value;
		//        RaisePropertyChanged("Port");
		//    }
		//}

		public string FavIconPathRelativeToBinFolder { get; private set; }

		public IMessageReporter MessageReporter { get; private set; }

		public RequestDelegate Request { get; private set; }

#if DEBUG
		~LocalHostHttpServer() {
			Debugger.Break(); // Who forgot to dispose the server?
		}
#endif

		void HandleRequestOnWorkerThread(IAsyncResult asyncResult) {
			var httpListenerLocal = (HttpListener)asyncResult.AsyncState;
			if (!httpListenerLocal.IsListening)
				return; // We may hit this when shutting down.

			var httpListenerContext = httpListenerLocal.EndGetContext(asyncResult);

			// Immediately start listening for another request even while we process this one.
			httpListener.BeginGetContext(HandleRequestOnWorkerThread, httpListenerLocal);

			try {
				// HINT: To force browser to ask for the favicon.ico, hit Ctrl-F5 (works in Chrome at least).
				if (httpListenerContext.Request.RawUrl == "/favicon.ico")
					using (var binaryReader = new BinaryReader(File.Open(FavIconPathRelativeToBinFolder, FileMode.Open))) {
						var byteArray = new byte[binaryReader.BaseStream.Length];
						binaryReader.ReadBytes(binaryReader.Read(byteArray, 0, byteArray.Length));
						httpListenerContext.Response.ContentLength64 = byteArray.Length;
						httpListenerContext.Response.OutputStream.Write(byteArray, 0, byteArray.Length);
					}
				else {
					var bytesResponse = Request(this, httpListenerContext.Request);
					//var stringResponse = Request(httpListenerContext.Request);
					//var byteArrayBuffer = Encoding.UTF8.GetBytes(stringResponse);
					httpListenerContext.Response.ContentLength64 = bytesResponse.Length;
					httpListenerContext.Response.OutputStream.Write(bytesResponse, 0, bytesResponse.Length);
				}
			}
			catch (Exception exception) {
				// Report the exception on the owning thread.
				Dispatcher.Invoke(DispatcherPriority.Normal, (Action)(() => MessageReporter.ReportMessage(exception.Message, exception.ToString(), MessageType.Console)));
			}
			finally {
				httpListenerContext.Response.OutputStream.Close();
			}
		}
	}
}
